Conversation
* feat(COMPT-40): implement createQuery factory - Add createQuery(keyFn, fetcher) returning QueryDefinition<TParams, TData> - TData and TParams fully inferred from fetcher signature, zero manual annotation - queryKey returns stable readonly tuple via keyFn - useQuery shorthand hook wraps useTanstackQuery with typed params - Export from src/index.ts - Add @tanstack/react-query as peerDependency (>=5) and devDependency - Add pnpm cssstyle override to fix Node.js v22 + jsdom@28 ESM compat issue - Fix duplicate import in vitest.config.ts - 9 tests, 100% coverage on createQuery.ts, 88.6% overall Closes COMPT-40 * chore: sync package-lock.json with @tanstack/react-query addition * chore: switch from pnpm to npm, sync lockfile * chore: fix prettier formatting across all files
- usePaginatedQuery(queryDef, params, options) supports mode: 'offset' | 'cursor' - Offset mode: page/pageSize (default 20)/nextPage/prevPage/totalPages - Cursor mode: fetchNextPage/hasNextPage/nextCursor via useInfiniteQuery - Both expose data as flat T[] array, isLoading, isFetching, isError, error - Offset uses useQuery with page in queryKey; cursor uses useInfiniteQuery - getCursor option required for cursor mode - Typed overloads: full inference, no TanStack internals exposed - 15 tests, 100% coverage on usePaginatedQuery.ts, 95.62% overall Closes COMPT-41
* feat(COMPT-42): implement createMutation and typed cache helpers - createMutation(fn) returns MutationDefinition with mutationFn and useMutation shorthand - useMutation exposes mutate/mutateAsync/isPending/isError/error/data/reset - invalidateQueries(client, queryDef, params?) uses queryDef key — no raw strings - setQueryData typed updater — wrong shape is TypeScript compile error - All exported from src/index.ts via src/query/index.ts - 18 tests (10 mutation + 8 cache), 100% coverage on both src files, 95.94% overall Closes COMPT-42 * chore: fix prettier formatting * fix: suppress eslint no-unused-vars on intentionally unused mutation param
- createQuery.test.tsx: queryKey shape, queryFn call, useQuery loading/success/error/enabled/rerender - usePaginatedQuery.test.tsx: offset page navigation, data shape, cursor fetchNextPage/hasNextPage/nextCursor - createMutation.test.tsx: idle state, mutate, isPending, data, isError, reset, mutateAsync - cacheHelpers.test.tsx: invalidateQueries marks stale + refetch, setQueryData direct/updater/hook reflect All 84 tests pass, 97.35% stmt coverage (target: 85%)
- Moved all co-located tests (src/query/*.test.tsx, src/index.test.ts) into src/__tests__/ - Merged unique tests from co-located files: definition shape, stable key, TData inference, mode assertions, initialPage, mutationFn direct call - Deleted: src/query/createQuery.test.tsx, cacheHelpers.test.tsx, createMutation.test.tsx, usePaginatedQuery.test.tsx, src/index.test.ts - 51 tests, all passing, no test files outside src/__tests__/
- Rewrote README as an end-to-end usage guide for @ciscode/query-kit - createQuery: key builder, fetcher, useQuery shorthand, direct key/fn access - usePaginatedQuery: offset mode (nextPage/prevPage) and cursor mode (fetchNextPage/hasNextPage) - createMutation + invalidateQueries full lifecycle example - setQueryData typed updater example - API reference table covering all exports - Peer dep @tanstack/react-query >=5 clearly stated - Changeset: minor bump to v0.1.0 (initial public release)
There was a problem hiding this comment.
Pull request overview
Introduces @ciscode/query-kit, a small typed wrapper layer around TanStack Query v5, along with accompanying tests, documentation, and CI workflow updates to support publishing and validation.
Changes:
- Added typed query/mutation factories (
createQuery,createMutation), cache helpers, and a pagination hook (usePaginatedQuery) undersrc/query/and exported them via the public API. - Added integration tests covering the new query-kit utilities and updated documentation to describe the new package usage.
- Updated package/workflow metadata for the new library name/versioning and strengthened CI (PR validation, release checks, publish workflow).
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
vitest.config.ts |
Removes duplicate import; keeps vitest setup consistent. |
src/query/usePaginatedQuery.ts |
Adds unified pagination hook over useQuery / useInfiniteQuery. |
src/query/index.ts |
Adds barrel exports for the new query-kit surface area. |
src/query/createQuery.ts |
Adds typed query definition factory with useQuery shorthand. |
src/query/createMutation.ts |
Adds typed mutation definition factory with useMutation shorthand. |
src/query/cacheHelpers.ts |
Adds typed cache helpers for invalidation and cache writes. |
src/index.ts |
Exposes query module as part of the library public API. |
src/__tests__/usePaginatedQuery.test.tsx |
Adds integration tests for offset + cursor pagination behavior. |
src/__tests__/createQuery.test.tsx |
Adds integration tests for query definition + shorthand hook. |
src/__tests__/createMutation.test.tsx |
Adds integration tests for mutation definition + shorthand hook. |
src/__tests__/cacheHelpers.test.tsx |
Adds integration tests for cache helper behavior. |
src/__tests__/index.test.ts |
Adds placeholder test file. |
README.md |
Replaces template README with package docs and examples for query-kit. |
package.json |
Renames package, adds peer/dev deps for TanStack Query, adds repository + overrides. |
package-lock.json |
Locks new dependencies and applies cssstyle override. |
.github/workflows/release-check.yml |
Updates CI behavior, Node version, and SonarCloud configuration. |
.github/workflows/publish.yml |
Updates publish validations, Node version, and uses --provenance. |
.github/workflows/pr-validation.yml |
Adds CI workflow for PRs into develop. |
.github/dependabot.yml |
Enables monthly Dependabot updates with grouping. |
.github/CODEOWNERS |
Adds CODEOWNERS for repo-wide ownership. |
.changeset/v0-1-0-initial-release.md |
Adds initial changeset for the public release. |
| params?: TParams, | ||
| ): Promise<void> { | ||
| const queryKey = | ||
| params !== undefined ? queryDef.queryKey(params) : queryDef.queryKey({} as TParams); | ||
| return client.invalidateQueries({ queryKey: queryKey as unknown[] }); |
There was a problem hiding this comment.
invalidateQueries without params currently calls queryDef.queryKey({} as TParams), which (a) can throw if keyFn expects required fields and (b) will not invalidate existing queries for paramized keys (e.g. ['posts', 1] won’t match ['posts', undefined]). Consider changing the API so the query definition provides a stable base key/prefix (or queryKey accepts params?: TParams and returns a prefix when omitted), or make params required and remove the no-params overload.
| params?: TParams, | |
| ): Promise<void> { | |
| const queryKey = | |
| params !== undefined ? queryDef.queryKey(params) : queryDef.queryKey({} as TParams); | |
| return client.invalidateQueries({ queryKey: queryKey as unknown[] }); | |
| params: TParams, | |
| ): Promise<void> { | |
| return client.invalidateQueries({ | |
| queryKey: queryDef.queryKey(params) as unknown[], | |
| }); |
| ### invalidateQueries | ||
|
|
||
| Marks all matching queries as stale so they re-fetch. Uses the query | ||
| definition's key builder — no raw strings. | ||
|
|
||
| ```ts | ||
| import { invalidateQueries } from '@ciscode/query-kit'; | ||
|
|
||
| // Invalidate a specific user | ||
| await invalidateQueries(queryClient, userQuery, { id: 42 }); | ||
|
|
||
| // Invalidate all queries matching the userQuery key prefix | ||
| await invalidateQueries(queryClient, userQuery); | ||
| ``` |
There was a problem hiding this comment.
The README claims await invalidateQueries(queryClient, userQuery); invalidates “all queries matching the userQuery key prefix”, but the current implementation builds a key from an empty object (queryKey({} as TParams)), which typically won’t match real paramized query keys. Either update the docs to reflect the actual behavior or adjust invalidateQueries to support true prefix invalidation.
| - name: Validate version tag and package.json | ||
| run: | | ||
| TAG=$(git describe --exact-match --tags HEAD 2>/dev/null || echo "") | ||
| if [[ -z "$TAG" ]]; then | ||
| echo "❌ No tag found on HEAD. This push did not include a version tag." | ||
| echo "To publish, merge to master with a tag: git tag v1.0.0 && git push origin master --tags" | ||
| PKG_VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/') | ||
| TAG="v${PKG_VERSION}" | ||
|
|
||
| if [[ -z "$PKG_VERSION" ]]; then | ||
| echo "❌ ERROR: Could not read version from package.json" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "❌ Invalid tag format: $TAG. Expected: v*.*.*" | ||
| echo "❌ ERROR: Invalid version format in package.json: '$PKG_VERSION'" | ||
| echo "Expected format: x.y.z (e.g., 1.0.0, 0.2.3)" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! git rev-parse "$TAG" >/dev/null 2>&1; then | ||
| echo "❌ ERROR: Tag $TAG not found!" | ||
| echo "" | ||
| echo "This typically happens when:" | ||
| echo " 1. You forgot to run 'npm version patch|minor|major' on your feature branch" | ||
| echo " 2. You didn't push the tag: git push origin <feat/your-feature> --tags" | ||
| echo " 3. The tag was created locally but never pushed to remote" | ||
| echo "" | ||
| echo "📋 Correct workflow:" | ||
| echo " 1. On feat/** or feature/**: npm version patch (or minor/major)" | ||
| echo " 2. Push branch + tag: git push origin feat/your-feature --tags" | ||
| echo " 3. PR feat/** → develop, then PR develop → master" | ||
| echo " 4. Workflow automatically triggers on master push" | ||
| echo "" | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
The tag validation only checks that v${package.json version} exists somewhere in the repo (git rev-parse "$TAG"), not that the tag points at the current commit being published. This can allow publishing code on master that isn’t actually tagged with the version. Consider verifying the tag is on HEAD (e.g., using git describe --exact-match --tags HEAD or checking git tag --points-at HEAD).
| -Dsonar.organization=${{ env.SONAR_ORGANIZATION }} | ||
| -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} | ||
| -Dsonar.sources=src | ||
| -Dsonar.tests=test |
There was a problem hiding this comment.
-Dsonar.tests=test points SonarCloud at a test/ directory that doesn’t exist in this repo (tests live under src/__tests__/). This will cause Sonar to misclassify tests (or ignore them) and can skew coverage/duplication metrics. Update sonar.tests to the actual test path(s) or remove it and rely on sonar.sources plus sonar.exclusions/sonar.test.inclusions as needed.
| -Dsonar.tests=test | |
| -Dsonar.tests=src/__tests__ |
| @@ -24,15 +14,11 @@ jobs: | |||
| runs-on: ubuntu-latest | |||
| timeout-minutes: 25 | |||
There was a problem hiding this comment.
This workflow no longer sets an explicit permissions: block (unlike publish.yml and pr-validation.yml). If the repo/org default token permissions are broader than needed, this job will run with unnecessary privileges. Consider adding permissions: contents: read (and only what Sonar/checkout require) to keep least-privilege consistent across workflows.
| timeout-minutes: 25 | |
| timeout-minutes: 25 | |
| permissions: | |
| contents: read |
|
|
||
| export interface CursorPaginatedResult<TData> extends PaginatedBase<TData> { | ||
| mode: 'cursor'; | ||
| fetchNextPage: () => void; |
There was a problem hiding this comment.
CursorPaginatedResult.fetchNextPage is typed as () => void, but it wraps infiniteQuery.fetchNextPage() which returns a Promise. Exposing it as void prevents consumers from awaiting page loads (and makes it harder to compose with async flows/tests). Consider returning the Promise and updating the type accordingly.
| fetchNextPage: () => void; | |
| fetchNextPage: () => Promise<unknown>; |
| it('without params — calls invalidateQueries once (does not throw)', async () => { | ||
| const client = makeClient(); | ||
| const spy = vi.spyOn(client, 'invalidateQueries'); | ||
|
|
||
| await expect(invalidateQueries(client, postsQueryDef)).resolves.toBeUndefined(); |
There was a problem hiding this comment.
The no-params test only asserts that invalidateQueries doesn’t throw and that client.invalidateQueries was called once. It doesn’t verify that the intended queries are actually invalidated/refetched (which is the main value of the no-params behavior). Consider adding an assertion that queries with different params (e.g. {id:1} and {id:2}) become invalidated when calling invalidateQueries(client, postsQueryDef).
* feat(COMPT-44): README guide + changeset for v0.1.0 - Rewrote README as an end-to-end usage guide for @ciscode/query-kit - createQuery: key builder, fetcher, useQuery shorthand, direct key/fn access - usePaginatedQuery: offset mode (nextPage/prevPage) and cursor mode (fetchNextPage/hasNextPage) - createMutation + invalidateQueries full lifecycle example - setQueryData typed updater example - API reference table covering all exports - Peer dep @tanstack/react-query >=5 clearly stated - Changeset: minor bump to v0.1.0 (initial public release) * fix(ci): correct sonar.tests path from 'test' to 'src/__tests__' Tests live in src/__tests__/, not test/. Also add sonar.exclusions and sonar.test.inclusions so source files and test files are correctly separated in SonarCloud analysis.
|



Summary
Why
Checklist
npm run lintpassesnpm run typecheckpassesnpm testpassesnpm run buildpassesnpx changeset) if this affects consumersNotes